package biz.yfsoft.app.fastframework.plugin.shiro;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jfinal.kit.EncryptionKit;
import com.jfinal.kit.StrKit;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Record;

public class FastJFinalRealm extends AuthorizingRealm {

	// TODO - complete JavaDoc

	/*--------------------------------------------
	|             C O N S T A N T S             |
	============================================*/
	/**
	 * The default query used to retrieve account data for the user.
	 */
	protected static final String DEFAULT_AUTHENTICATION_QUERY = "SELECT pass_encode,login_pass FROM sys_user WHERE login_name = ?";

	/**
	 * The default query used to retrieve the roles that apply to a user.
	 */
	protected static final String DEFAULT_USER_ROLES_QUERY = "SELECT r.name FROM sys_user u,sys_role r,sys_user_role ur WHERE ur.roleid = r.id and ur.userid = u.id and login_name = ?";

	/**
	 * The default query used to retrieve permissions that apply to a particular
	 * role.
	 */
	protected static final String DEFAULT_PERMISSIONS_QUERY = "SELECT p.name FROM sys_permission p WHERE p.id in ?";

	private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class);

	/*--------------------------------------------
	|    I N S T A N C E   V A R I A B L E S    |
	============================================*/
	// protected DataSource dataSource;

	protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;

	protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;

	protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;

	protected boolean permissionsLookupEnabled = true;

	/*--------------------------------------------
	|         C O N S T R U C T O R S           |
	============================================*/

	public FastJFinalRealm() {
		log.info("FastJdbcRealm Init...");
	}

	/*--------------------------------------------
	|  A C C E S S O R S / M O D I F I E R S    |
	============================================*/

	/**
	 * Overrides the default query used to retrieve a user's password during
	 * authentication. When using the default implementation, this query must
	 * take the user's username as a single parameter and return a single result
	 * with the user's password as the first column. If you require a solution
	 * that does not match this query structure, you can override
	 * {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)}
	 * or just {@link #getPasswordForUser(java.sql.Connection,String)}
	 *
	 * @param authenticationQuery
	 *            the query to use for authentication.
	 * @see #DEFAULT_AUTHENTICATION_QUERY
	 */
	public void setAuthenticationQuery(String authenticationQuery) {
		this.authenticationQuery = authenticationQuery;
	}

	/**
	 * Overrides the default query used to retrieve a user's roles during
	 * authorization. When using the default implementation, this query must
	 * take the user's username as a single parameter and return a row per role
	 * with a single column containing the role name. If you require a solution
	 * that does not match this query structure, you can override
	 * {@link #doGetAuthorizationInfo(PrincipalCollection)} or just
	 * {@link #getRoleNamesForUser(java.sql.Connection,String)}
	 *
	 * @param userRolesQuery
	 *            the query to use for retrieving a user's roles.
	 * @see #DEFAULT_USER_ROLES_QUERY
	 */
	public void setUserRolesQuery(String userRolesQuery) {
		this.userRolesQuery = userRolesQuery;
	}

	/**
	 * Overrides the default query used to retrieve a user's permissions during
	 * authorization. When using the default implementation, this query must
	 * take a role name as the single parameter and return a row per permission
	 * with three columns containing the fully qualified name of the permission
	 * class, the permission name, and the permission actions (in that order).
	 * If you require a solution that does not match this query structure, you
	 * can override
	 * {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)}
	 * or just
	 * {@link #getPermissions(java.sql.Connection,String,java.util.Collection)}
	 * </p>
	 * <p/>
	 * <b>Permissions are only retrieved if you set
	 * {@link #permissionsLookupEnabled} to true. Otherwise, this query is
	 * ignored.</b>
	 *
	 * @param permissionsQuery
	 *            the query to use for retrieving permissions for a role.
	 * @see #DEFAULT_PERMISSIONS_QUERY
	 * @see #setPermissionsLookupEnabled(boolean)
	 */
	public void setPermissionsQuery(String permissionsQuery) {
		this.permissionsQuery = permissionsQuery;
	}

	/**
	 * Enables lookup of permissions during authorization. The default is
	 * "false" - meaning that only roles are associated with a user. Set this to
	 * true in order to lookup roles <b>and</b> permissions.
	 *
	 * @param permissionsLookupEnabled
	 *            true if permissions should be looked up during authorization,
	 *            or false if only roles should be looked up.
	 */
	public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) {
		this.permissionsLookupEnabled = permissionsLookupEnabled;
	}

	/*--------------------------------------------
	|               M E T H O D S               |
	============================================*/

	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {

		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		String username = upToken.getUsername();

		// Null username is invalid
		if (username == null) {
			throw new AccountException(
					"Null usernames are not allowed by this realm.");
		}

		AuthenticationInfo info = null;
		try {

			Record password = getPasswordForUser(username);

			if (password == null) {
				throw new UnknownAccountException("No account found for user ["
						+ username + "]");
			}

			String pass = new String(upToken.getPassword());
			String encodePass = "";
			if (StrKit.notBlank(password.getStr("pass_encode"))) {
				encodePass = EncryptionKit.encrypt(password.getStr("pass_encode"),
						pass);
			}
			
			if (!encodePass.equals(password.get("login_pass"))) {
				throw new IncorrectCredentialsException(
						"Password Incorrect For User [" + username + "]");
			}

			info = buildAuthenticationInfo(username, pass.toCharArray());
		} catch (AuthenticationException e) {
			final String message = "There was a SQL error while authenticating user ["
					+ username + "]";
			if (log.isErrorEnabled()) {
				log.error(message, e);
			}
			// Rethrow any SQL errors as an authentication exception
			throw new AuthenticationException(message, e);
		}

		return info;
	}

	protected AuthenticationInfo buildAuthenticationInfo(String username,
			char[] password) {
		return new SimpleAuthenticationInfo(username, password, getName());
	}

	private Record getPasswordForUser(String username)
			throws AuthenticationException {
		Record user = null;
		try {
			user = Db.findFirst(authenticationQuery, username);
			if (user == null) {
				throw new AuthenticationException("No User Named: [" + username
						+ "].");
			} else {
				return user;
			}
		} catch (AuthenticationException hex) {
			throw hex;
		}
	}

	/**
	 * This implementation of the interface expects the principals collection to
	 * return a String username keyed off of this realm's {@link #getName()
	 * name}
	 *
	 * @see #getAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection principals) {

		// null usernames are invalid
		if (principals == null) {
			throw new AuthorizationException(
					"PrincipalCollection method argument cannot be null.");
		}

		String username = (String) getAvailablePrincipal(principals);

		Set<String> roleNames = null;
		Set<String> permissions = null;
		// Retrieve roles and permissions from database
		roleNames = getRoleNamesForUser(username);
		if (permissionsLookupEnabled) {
			permissions = getPermissions(roleNames);
		}
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
		info.setStringPermissions(permissions);
		return info;

	}

	protected Set<String> getRoleNamesForUser(String username) {
		Set<String> roleNames = new LinkedHashSet<String>();
		List<Record> list = Db.findByCache("roles", "user" + "_" + username,
				userRolesQuery, username);
		if (list != null) {
			for (Record r : list) {
				roleNames.add(r.getStr("name"));
			}
		}
		return roleNames;
	}

	protected Set<String> getPermissions(Collection<String> roleNames) {
		Set<String> permissions = new LinkedHashSet<String>();
		for (String roleName : roleNames) {
			List<Record> records = Db
					.findByCache(
							"permissions",
							"roleList" + roleNames,
							"SELECT rp.permissionid as permissionids FROM sys_role r,sys_role_permission rp WHERE rp.roleid = r.id and r.name = ?",
							roleName);
			if(records != null && records.size() > 0){
				Object[] permissionids = records.get(0).get("permissionids")
						.toString().split(",");
				StringBuffer sb = new StringBuffer(
						"SELECT p.name FROM sys_permission p WHERE p.id in (");
				for (int i = 0; i < permissionids.length; i++) {
					sb.append("?");
					if (permissionids.length > (i + 1))
						sb.append(",");
				}
				sb.append(")");
				
				List<Record> list = Db.findByCache("permissions", "permissionList"
						+ roleNames, sb.toString(), permissionids);
				if (list != null) {
					for (Record r : list) {
						permissions.add(r.getStr("name"));
					}
				}
			}
		}
		return permissions;
	}

}